/*
 * jQuery JSONP Core Plugin 2.3.0 (2012-03-27)
 *
 * https://github.com/jaubourg/jquery-jsonp
 *
 * Copyright (c) 2012 Julian Aubourg
 *
 * This document is licensed as free software under the terms of the
 * MIT License: http://www.opensource.org/licenses/mit-license.php
 */
(function ($) {

    // ###################### UTILITIES ##

    // Noop
    function noop() {
    }

    // Generic callback
    function genericCallback(data) {
        lastValue = [data];
    }

    // Call if defined
    function callIfDefined(method, object, parameters, returnFlag) {
        try {
            returnFlag = method && method.apply(object.context || object, parameters);
        } catch (_) {
            returnFlag = !1;
        }
        return returnFlag;
    }

    // Give joining character given url
    function qMarkOrAmp(url) {
        return /\?/.test(url) ? "&" : "?";
    }

    var // String constants (for better minification)
		STR_ASYNC = "async",
		STR_CHARSET = "charset",
		STR_EMPTY = "",
		STR_ERROR = "error",
		STR_INSERT_BEFORE = "insertBefore",
		STR_JQUERY_JSONP = "_jqjsp",
		STR_ON = "on",
		STR_ON_CLICK = STR_ON + "click",
		STR_ON_ERROR = STR_ON + STR_ERROR,
		STR_ON_LOAD = STR_ON + "load",
		STR_ON_READY_STATE_CHANGE = STR_ON + "readystatechange",
		STR_READY_STATE = "readyState",
		STR_REMOVE_CHILD = "removeChild",
		STR_SCRIPT_TAG = "<script>",
		STR_SUCCESS = "success",
		STR_TIMEOUT = "timeout",

    // Window
		win = window,
    // Deferred
		Deferred = $.Deferred,
    // Head element
		head = $("head")[0] || document.documentElement,
    // First child
		firstChild = head.firstChild,
    // Page cache
		pageCache = {},
    // Counter
		count = 0,
    // Last returned value
		lastValue,

    // ###################### DEFAULT OPTIONS ##
		xOptionsDefaults = {
		    //beforeSend: undefined,
		    //cache: false,
		    callback: STR_JQUERY_JSONP,
		    //callbackParameter: undefined,
		    //charset: undefined,
		    //complete: undefined,
		    //context: undefined,
		    //data: "",
		    //dataFilter: undefined,
		    //error: undefined,
		    //pageCache: false,
		    //success: undefined,
		    //timeout: 0,
		    //traditional: false,
		    url: location.href
		},

    // opera demands sniffing :/
		opera = win.opera;

    // ###################### MAIN FUNCTION ##
    function jsonp(xOptions) {

        // Build data with default
        xOptions = $.extend({}, xOptionsDefaults, xOptions);

        // References to xOptions members (for better minification)
        var successCallback = xOptions.success,
			errorCallback = xOptions.error,
			completeCallback = xOptions.complete,
			dataFilter = xOptions.dataFilter,
			callbackParameter = xOptions.callbackParameter,
			successCallbackName = xOptions.callback,
			cacheFlag = xOptions.cache,
			pageCacheFlag = xOptions.pageCache,
			charset = xOptions.charset,
			url = xOptions.url,
			data = xOptions.data,
			timeout = xOptions.timeout,
			pageCached,

        // Abort/done flag
			done = 0,

        // Life-cycle functions
			cleanUp = noop,

        // Support vars
			supportOnload,
			supportOnreadystatechange,

        // Request execution vars
			script,
			scriptAfter,
			timeoutTimer;

        // If we have Deferreds:
        // - substitute callbacks
        // - promote xOptions to a promise
        Deferred && Deferred(function (defer) {
            defer.done(successCallback).fail(errorCallback);
            successCallback = defer.resolve;
            errorCallback = defer.reject;
        }).promise(xOptions);

        // Create the abort method
        xOptions.abort = function () {
            !(done++) && cleanUp();
        };

        // Call beforeSend if provided (early abort if false returned)
        if (callIfDefined(xOptions.beforeSend, xOptions, [xOptions]) === !1 || done) {
            return xOptions;
        }

        // Control entries
        url = url || STR_EMPTY;
        data = data ? ((typeof data) == "string" ? data : $.param(data, xOptions.traditional)) : STR_EMPTY;

        // Build final url
        url += data ? (qMarkOrAmp(url) + data) : STR_EMPTY;

        // Add callback parameter if provided as option
        callbackParameter && (url += qMarkOrAmp(url) + encodeURIComponent(callbackParameter) + "=?");

        // Add anticache parameter if needed
        !cacheFlag && !pageCacheFlag && (url += qMarkOrAmp(url) + "_" + (new Date()).getTime() + "=");

        // Replace last ? by callback parameter
        url = url.replace(/=\?(&|$)/, "=" + successCallbackName + "$1");
        // Success notifier
        function notifySuccess(json) {

            if (!(done++)) {

                cleanUp();
                // Pagecache if needed
                pageCacheFlag && (pageCache[url] = { s: [json] });
                // Apply the data filter if provided
                dataFilter && (json = dataFilter.apply(xOptions, [json]));
                // Call success then complete
                callIfDefined(successCallback, xOptions, [json, STR_SUCCESS]);
                callIfDefined(completeCallback, xOptions, [xOptions, STR_SUCCESS]);

            }
        }

        // Error notifier
        function notifyError(type) {

            if (!(done++)) {

                // Clean up
                cleanUp();
                // If pure error (not timeout), cache if needed
                pageCacheFlag && type != STR_TIMEOUT && (pageCache[url] = type);
                // Call error then complete
                callIfDefined(errorCallback, xOptions, [xOptions, type]);
                callIfDefined(completeCallback, xOptions, [xOptions, type]);

            }
        }

        // Check page cache
        if (pageCacheFlag && (pageCached = pageCache[url])) {

            pageCached.s ? notifySuccess(pageCached.s[0]) : notifyError(pageCached);

        } else {

            // Install the generic callback
            // (BEWARE: global namespace pollution ahoy)
            win[successCallbackName] = genericCallback;

            // Create the script tag
            script = $(STR_SCRIPT_TAG)[0];
            script.id = STR_JQUERY_JSONP + count++;

            // Set charset if provided
            if (charset) {
                script[STR_CHARSET] = charset;
            }

            opera && opera.version() < 11.60 ?
            // onerror is not supported: do not set as async and assume in-order execution.
            // Add a trailing script to emulate the event
				((scriptAfter = $(STR_SCRIPT_TAG)[0]).text = "document.getElementById('" + script.id + "')." + STR_ON_ERROR + "()")
			:
            // onerror is supported: set the script as async to avoid requests blocking each others
				(script[STR_ASYNC] = STR_ASYNC)

			;

            // Internet Explorer: event/htmlFor trick
            if (STR_ON_READY_STATE_CHANGE in script) {

                script.htmlFor = script.id;
                script.event = STR_ON_CLICK;
            }

            // Attached event handlers
            script[STR_ON_LOAD] = script[STR_ON_ERROR] = script[STR_ON_READY_STATE_CHANGE] = function (result) {

                // Test readyState if it exists
                if (!script[STR_READY_STATE] || !/i/.test(script[STR_READY_STATE])) {

                    try {

                        script[STR_ON_CLICK] && script[STR_ON_CLICK]();

                    } catch (_) { }

                    result = lastValue;
                    lastValue = 0;
                    result ? notifySuccess(result[0]) : notifyError(STR_ERROR);

                }
            };

            // Set source
            script.src = url;

            // Re-declare cleanUp function
            cleanUp = function (i) {
                timeoutTimer && clearTimeout(timeoutTimer);
                script[STR_ON_READY_STATE_CHANGE] = script[STR_ON_LOAD] = script[STR_ON_ERROR] = null;
                head[STR_REMOVE_CHILD](script);
                scriptAfter && head[STR_REMOVE_CHILD](scriptAfter);
            };

            // Append main script
            head[STR_INSERT_BEFORE](script, firstChild);

            // Append trailing script if needed
            scriptAfter && head[STR_INSERT_BEFORE](scriptAfter, firstChild);

            // If a timeout is needed, install it
            timeoutTimer = timeout > 0 && setTimeout(function () {
                notifyError(STR_TIMEOUT);
            }, timeout);

        }

        return xOptions;
    }

    // ###################### SETUP FUNCTION ##
    jsonp.setup = function (xOptions) {
        $.extend(xOptionsDefaults, xOptions);
    };

    // ###################### INSTALL in jQuery ##
    $.jsonp = jsonp;

})(jQuery);
